package com.ruyuan.process.engine.process;

import com.ruyuan.process.engine.enums.InvokeMethod;
import com.ruyuan.process.engine.node.ProcessorDefinition;
import com.ruyuan.process.engine.node.ProcessorNode;
import com.ruyuan.process.engine.utils.ProcessorUtils;
import lombok.extern.slf4j.Slf4j;

import java.util.HashMap;
import java.util.Map;
import java.util.Stack;

/**
 * 流程上下文
 *
 * @author zhonghuashishan
 * @version 1.0
 */
@Slf4j
public class ProcessContext {

    private final Map<String, Object> params = new HashMap<>();

    private final ProcessorDefinition processorDefinition;

    // 每个线程都有自己的ProcessContext，一个ProcessContext只有一个线程驱动一次流程实例运行的时候才会访问
    // 对于ProcessContext里的数据结构，他没有任何多线程并发的访问问题
    // 直接就用一个JDK体用的Stack数据结构就可以了
    private final Stack<RollbackProcessor> rollbackProcessors = new Stack<>();

    public ProcessContext(ProcessorDefinition processorDefinition) {
        this.processorDefinition = processorDefinition;
    }

    public void set(String key, Object value) {
        params.put(key, value);
    }

    @SuppressWarnings("unchecked")
    public <T> T get(String key) {
        return (T) params.get(key);
    }

    /**
     * 启动
     */
    public void start() {
        log.info("流程名称+流程实例id started");
        process(processorDefinition.getFirst());
        log.info("流程名称+流程实例id finished");
    }

    private void process(ProcessorNode node) {
        if (node == null) {
            return;
        }
        Processor processor = node.getProcessor();
        try {
            // 如果说你要是支持回滚的Rollback的流程节点，就必须把你push到一个栈数据结构里去
            // 队列数据结构，他是先入先出，谁先进去谁先出来
            // 栈数据结构，先入后出，谁先进去，谁就最后一个出来，栈数据结构
            if (processor instanceof RollbackProcessor) {
                rollbackProcessors.push((RollbackProcessor) processor);
            }

            processor.process(this);
            log.info("流程名称+流程实例id+流程节点名称 finished context");
        } catch (Exception e) {
            log.error("流程名称+流程实例id+流程节点名称 error context");

            // 回滚前面的所有可回滚节点，按照所有节点的顺序倒叙回滚
            RollbackProcessor rollbackProcessor;

            // while循环，不断的去判断stack栈是否为空，如果栈不为空
            while (!rollbackProcessors.empty()) {
                rollbackProcessor = rollbackProcessors.pop(); // 把栈顶的数据先弹出来
                try {
                    // 只有支持rollback的节点才会在栈里，从栈顶开始一个一个的pop出来
                    rollbackProcessor.rollback(this); // 运行他的rollback逻辑，只有支持rollback的节点类型，才会在运行前压入栈
                    log.info("流程名称+流程实例id+流程节点名称 rollback finished context");
                } catch (Exception e1) {
                    // 如果回滚过程中也抛异常了，此时暂时没有什么好的办法处理。
                    log.error("流程节点在回滚过程中抛出异常，process={}, context={}",
                            rollbackProcessor.getName(), params.values());

                    // 我们需要把哪些东西记录一下日志，才能支持我们人工介入之后，手工写代码，去重复的执行回滚逻辑，把回滚给他做了
                    // 查log error，流程名称，回滚节点是属于哪个流程，节点名称，执行回滚逻辑异常，上下文数据是什么样的
                    // 从日志里拿到就可以了，必须接入公司的报警API，短信、钉钉去进行报警
                    // 直接查日志就可以了，error，写一段程序，针对这个流程的指定节点，调用他的rollback回滚逻辑，把上下文数据还原出来给他
                    // 专门让他去执行回滚就可以了，这个就是典型的人工介入
                }
            }
            log.info("流程名称+流程实例id rollback finished");
            processor.caughtException(this, e);
            // 流程出现问题的时候，需要将异常往外抛，让业务方处理.
            throw e;
        }

        ProcessorNode nextNode = null;
        Map<String, ProcessorNode> nextNodes = node.getNextNodes();
        if (processor instanceof DynamicProcessor) {
            // 动态计算出下一个节点
            DynamicProcessor dynamicProcessor = (DynamicProcessor) processor; // 从高层次的接口类型，强制转换为低层次的具体抽象类的类型
            String nextNodeId = dynamicProcessor.nextNodeId(this);
            if (!nextNodes.containsKey(nextNodeId)) {
                throw new IllegalArgumentException("DynamicProcess can not find next node with id:" + nextNodeId);
            }
            nextNode = nextNodes.get(nextNodeId);
        } else {
            // 不是动态节点，每个节点只有一个后继节点
            if (!nextNodes.isEmpty()) {
                nextNode = nextNodes.values().stream().findAny().get();
            }
        }

        // 当你的下一个节点拿到了之后，在调用下一个节点的时候，会根据你的invoke method，决定是同步调用，还是异步调用
        // 流程节点之间进行流转的时候，是可以同步调用，也可以异步调用

        InvokeMethod invokeMethod = node.getInvokeMethod();
        if (invokeMethod.equals(InvokeMethod.SYNC)) {
            process(nextNode);
        } else {
            ProcessorNode finalNextNode = nextNode;
            ProcessorUtils.executeAsync(() -> process(finalNextNode));
        }
    }
}
